{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## LangGraph SQL Agent 中文指南\n",
    "\n",
    "本指南详细展示了如何构建一个能够回答关于 SQL 数据库问题的 Agent，结合了 LangGraph、LangChain 和 OpenAI 的能力，能够处理复杂的 SQL 查询并提供智能化的交互式响应。\n",
    "\n",
    "### 总体流程\n",
    "\n",
    "Agent 将执行以下任务：\n",
    "1. 获取数据库中的可用表。\n",
    "2. 判断哪些表与问题相关。\n",
    "3. 获取相关表的 DDL（数据定义语言）。\n",
    "4. 根据问题和 DDL 信息生成 SQL Query\n",
    "5. 使用 LLM（大语言模型）检查 SQL Query 中的常见错误。\n",
    "6. 执行查询并返回结果。\n",
    "7. 如果数据库引擎返回错误，纠正查询中的错误，直到查询成功。\n",
    "8. 根据查询结果生成最终回答。\n",
    "\n",
    "### 完整的工作流程如下：\n",
    "\n",
    "### 环境配置\n",
    "首先，我们将配置 OpenAI 和可选的 LangSmith API 环境变量，并启用 LangSmith 的追踪功能。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "# 设置 API 密钥和 LangSmith 追踪功能\n",
    "os.environ[\"LANGCHAIN_PROJECT\"] = \"SQL Agent\"  # 为项目命名"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 安装必要的软件包\n",
    "我们将使用 `langchain_community` 包中的 SQL 数据库封装器与数据库交互，还会使用 `langchain_openai` 包与 OpenAI API 进行交互。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%capture --no-stderr\n",
    "# 安装 LangGraph 和 LangSmith，用于状态图和跟踪\n",
    "%pip install -U langgraph langsmith langchain_openai"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 配置数据库\n",
    "\n",
    "我们将创建一个 SQLite 数据库并加载示例数据库——Chinook。\n",
    "\n",
    "Chinook 是一个代表数字媒体商店的样例数据库。可以在 [Chinook主页](https://github.com/lerocha/chinook-database) 找到更多信息。\n",
    "\n",
    "![chinook_db](images/chinook.png)\n",
    "\n",
    "\n",
    "### 📢 Callback: OpenAI Function Call\n",
    "\n",
    "在介绍 OpenAI Function Call 功能时，我们也曾使用 Chinook 数据库作为样例。\n",
    "\n",
    "复习请跳转：[使用 OpenAI Function Call 执行 GPT 模型生成的函数](../../openai_api/function_call.ipynb)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "文件已下载并保存为 Chinook.db\n"
     ]
    }
   ],
   "source": [
    "import requests\n",
    "\n",
    "# 下载 Chinook 数据库\n",
    "url = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\n",
    "\n",
    "response = requests.get(url)\n",
    "if response.status_code == 200:\n",
    "    # 将下载的内容保存为 Chinook.db\n",
    "    with open(\"data/Chinook.db\", \"wb\") as file:\n",
    "        file.write(response.content)\n",
    "    print(\"文件已下载并保存为 Chinook.db\")\n",
    "else:\n",
    "    print(f\"文件下载失败，状态码: {response.status_code}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sqlite\n",
      "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\""
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 导入 SQLDatabase 模块\n",
    "from langchain_community.utilities import SQLDatabase\n",
    "\n",
    "# 连接 SQLite 数据库\n",
    "db = SQLDatabase.from_uri(\"sqlite:///data/Chinook.db\")\n",
    "print(db.dialect)\n",
    "print(db.get_usable_table_names())\n",
    "\n",
    "# 执行 SQL 查询\n",
    "db.run(\"SELECT * FROM Artist LIMIT 10;\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---------\n",
    "\n",
    "#### 输出结果应如下所示：\n",
    "\n",
    "```shell\n",
    "sqlite\n",
    "```\n",
    "\n",
    "```sql\n",
    "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n",
    "[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 定义辅助函数\n",
    "\n",
    "我们将定义一些实用函数来帮助实现 Agent，具体来说：\n",
    "\n",
    "- 定义 ToolNode 并设置回退机制，以处理错误并将其传递给 Agent。\n",
    "- 当一个 Agent 尝试调用某个工具并发生错误时，`handle_tool_error` 函数将被触发，将错误信息以适当的格式反馈给 Agent，允许 Agent 进行进一步的操作（例如重试或提示用户修复）。\n",
    "\n",
    "\n",
    "#### 代码说明：\n",
    "\n",
    "1. **`create_tool_node_with_fallback` 函数**：\n",
    "   - 该函数用于创建一个 `ToolNode`，它是 LangGraph 中用于执行某些工具操作的节点。\n",
    "   - 该节点带有回退机制，意味着如果工具调用失败，系统将使用回退逻辑来处理错误。\n",
    "   - 回退逻辑使用了 `RunnableLambda` 绑定到 `handle_tool_error` 函数，以便在遇到错误时生成错误消息，并反馈给 Agent。\n",
    "\n",
    "2. **`handle_tool_error` 函数**：\n",
    "   - 该函数负责在工具调用出错时处理错误。它从 `state` 中获取错误信息和工具调用记录，并生成一条包含错误描述的 `ToolMessage`。\n",
    "   - 该消息包含了错误详情（`error`）以及相关工具调用的 ID，以便后续可以追踪该错误与哪个工具调用有关。\n",
    "\n",
    "3. **回退机制**：\n",
    "   - 在调用某些工具（如 SQL 查询或 API 请求）时，可能会出现错误。为了确保系统能有效处理这些错误并继续工作，回退机制在错误发生时被触发，并通过 `handle_tool_error` 函数将错误信息返回给系统或用户。这种设计提高了系统的鲁棒性。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Any\n",
    "from langchain_core.messages import ToolMessage\n",
    "from langchain_core.runnables import RunnableLambda, RunnableWithFallbacks\n",
    "from langgraph.prebuilt import ToolNode\n",
    "\n",
    "# 创建具有回退机制的工具节点\n",
    "def create_tool_node_with_fallback(tools: list) -> RunnableWithFallbacks[Any, dict]:\n",
    "    \"\"\"\n",
    "    创建一个 ToolNode（工具节点），并为它添加回退机制。回退机制用于\n",
    "    在工具调用出现错误时处理这些错误，并将错误信息传递给 Agent。\n",
    "    \n",
    "    tools: 传入的工具列表，每个工具可以执行某种操作\n",
    "    返回值: 包含回退逻辑的 ToolNode\n",
    "    \"\"\"\n",
    "    return ToolNode(tools).with_fallbacks(\n",
    "        # 添加回退逻辑，使用 RunnableLambda 运行 handle_tool_error 方法来处理错误\n",
    "        [RunnableLambda(handle_tool_error)], \n",
    "        # 指定当出现 \"error\" 时触发回退机制\n",
    "        exception_key=\"error\"\n",
    "    )\n",
    "\n",
    "# 处理工具调用时发生的错误\n",
    "def handle_tool_error(state) -> dict:\n",
    "    \"\"\"\n",
    "    处理工具调用过程中发生的错误，并返回包含错误信息的消息列表。\n",
    "\n",
    "    state: 当前的状态，包含工具调用的信息和发生的错误\n",
    "    返回值: 包含错误信息的消息字典\n",
    "    \"\"\"\n",
    "    # 获取错误信息\n",
    "    error = state.get(\"error\")\n",
    "    \n",
    "    # 获取最后一个消息中的工具调用列表\n",
    "    tool_calls = state[\"messages\"][-1].tool_calls\n",
    "    \n",
    "    # 返回带有错误信息的 ToolMessage 列表\n",
    "    return {\n",
    "        \"messages\": [\n",
    "            ToolMessage(\n",
    "                content=f\"Error: {repr(error)}\\n please fix your mistakes.\",  # 生成错误内容\n",
    "                tool_call_id=tc[\"id\"],  # 记录工具调用的唯一ID\n",
    "            )\n",
    "            for tc in tool_calls  # 为每个工具调用创建对应的错误消息\n",
    "        ]\n",
    "    }\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 配置大模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# 初始化用于 SQL 数据库表获取的语言模型（服务于SQLDatabaseToolkit）\n",
    "sql_llm = ChatOpenAI(model=\"gpt-4o\")\n",
    "\n",
    "# 初始化用于查询的语言模型（服务于自定义的工具：如 db_query_tool）\n",
    "query_llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 定义 Agent 的工具\n",
    "\n",
    "下面我们定义一些工具来让 Agent 与 SQLite 数据库交互，并通过 LLM（大型语言模型）帮助生成和优化 SQL 查询。\n",
    "\n",
    "前2个工具可以直接从`SQLDatabaseToolkit`获取，`db_query_tool`需要手动实现：\n",
    "\n",
    "1. `list_tables_tool`: 获取数据库中的可用表。\n",
    "2. `get_schema_tool`: 获取表的 DDL（数据定义语言）。\n",
    "3. `db_query_tool`: 执行 SQL 查询并获取结果。\n",
    "\n",
    "用户可以通过这些工具获取数据库表的信息以及每个表的架构，这对于自动化 SQL 查询生成和数据库管理非常有用。\n",
    "\n",
    "#### 代码说明：\n",
    "1. **创建 LLM 实例**：\n",
    "   - `sql_llm` 是一个用于生成查询的 LLM 实例，使用的是 `gpt-4o-mini` 模型。这个模型可能用于理解用户的问题并生成相应的 SQL 查询。\n",
    "   - `query_llm` 用于查询验证，它的 `temperature=0` 表示该模型将生成确定性输出，适合用于精确的 SQL 查询检查或执行。\n",
    "\n",
    "2. **创建 SQL 工具包**：\n",
    "   - `SQLDatabaseToolkit` 是一个工具包，它集成了一个 SQL 数据库（通过 `db` 对象传入）和一个 LLM（`sql_llm`），允许通过自然语言与数据库进行交互。\n",
    "   - `get_tools()` 函数返回工具包中的一系列工具，用于处理不同的 SQL 操作。\n",
    "\n",
    "3. **获取特定工具**：\n",
    "   - 通过 `next()` 从工具列表中选择出两个特定的工具：\n",
    "     - `list_tables_tool`：用于列出数据库中的所有表。\n",
    "     - `get_schema_tool`：用于获取某张表的架构信息（DDL）。\n",
    "\n",
    "4. **调用工具**：\n",
    "   - `list_tables_tool.invoke(\"\")`：调用该工具来列出数据库中的所有表，并打印输出。\n",
    "   - `get_schema_tool.invoke(\"Artist\")`：调用该工具获取 `Artist` 表的架构信息，并打印输出。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n",
      "\n",
      "CREATE TABLE \"Artist\" (\n",
      "\t\"ArtistId\" INTEGER NOT NULL, \n",
      "\t\"Name\" NVARCHAR(120), \n",
      "\tPRIMARY KEY (\"ArtistId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Artist table:\n",
      "ArtistId\tName\n",
      "1\tAC/DC\n",
      "2\tAccept\n",
      "3\tAerosmith\n",
      "*/\n"
     ]
    }
   ],
   "source": [
    "from langchain_community.agent_toolkits import SQLDatabaseToolkit\n",
    "\n",
    "# 使用数据库连接和 SQL 语言模型创建 SQL 数据库工具包\n",
    "toolkit = SQLDatabaseToolkit(db=db, llm=sql_llm)\n",
    "\n",
    "# 从工具包中获取所有可用的工具\n",
    "tools = toolkit.get_tools()\n",
    "\n",
    "# 从工具列表中获取用于列出数据库中所有表的工具\n",
    "list_tables_tool = next(tool for tool in tools if tool.name == \"sql_db_list_tables\")\n",
    "\n",
    "# 从工具列表中获取用于获取指定表的模式（schema）的工具\n",
    "get_schema_tool = next(tool for tool in tools if tool.name == \"sql_db_schema\")\n",
    "\n",
    "# 调用列出表的工具，并打印输出结果\n",
    "print(list_tables_tool.invoke(\"\"))\n",
    "\n",
    "# 调用获取 \"Artist\" 表模式的工具，并打印输出结果\n",
    "print(get_schema_tool.invoke(\"Artist\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---------\n",
    "\n",
    "#### 输出应如下所示：\n",
    "\n",
    "```sql\n",
    "Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n",
    "\n",
    "CREATE TABLE \"Artist\" (\n",
    "\t\"ArtistId\" INTEGER NOT NULL, \n",
    "\t\"Name\" NVARCHAR(120), \n",
    "\tPRIMARY KEY (\"ArtistId\")\n",
    ")\n",
    "\n",
    "/*\n",
    "3 rows from Artist table:\n",
    "ArtistId\tName\n",
    "1\tAC/DC\n",
    "2\tAccept\n",
    "3\tAerosmith\n",
    "*/\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 定义工具 `db_query_tool`\n",
    "\n",
    "\n",
    "`db_query_tool` 工具在接收到查询后，执行数据库操作并返回结果。\n",
    "\n",
    "如果查询失败，工具会向用户反馈并要求重写查询。这种工具在与数据库交互的自动化应用中非常有用，例如报告生成、数据查询等场景。\n",
    "\n",
    "#### 代码说明：\n",
    "1. **`db_query_tool` 函数**：\n",
    "   - 这是一个使用 `@tool` 装饰器定义的 SQL 查询工具，允许智能体（agent）通过自然语言调用它来执行 SQL 查询。\n",
    "   - `query` 是传入的 SQL 查询字符串。\n",
    "   - `db.run_no_throw(query)` 是用于执行 SQL 查询的方法，它保证在执行过程中即使发生错误也不会抛出异常，而是返回 `None` 或其他可处理的结果。\n",
    "\n",
    "2. **错误处理**：\n",
    "   - 如果查询失败，`run_no_throw` 会返回 `None` 或空值，这时函数将返回一个错误信息，提示用户需要重写查询。\n",
    "   - 如果查询成功，函数会返回查询的结果。\n",
    "\n",
    "3. **工具调用**：\n",
    "   - `db_query_tool.invoke(\"SELECT * FROM Artist LIMIT 10;\")`：通过 `invoke` 方法执行 SQL 查询。这种调用方式适合在智能体工作流程中使用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.tools import tool\n",
    "\n",
    "# 定义 SQL 查询工具\n",
    "@tool  # 使用 @tool 装饰器将函数定义为一个工具\n",
    "def db_query_tool(query: str) -> str:\n",
    "    \"\"\"\n",
    "    对数据库执行 SQL 查询并获取结果。\n",
    "    如果查询不正确，将返回错误消息，并提示用户重写查询。\n",
    "\n",
    "    参数：\n",
    "    query (str): 传入的 SQL 查询字符串\n",
    "\n",
    "    返回值：\n",
    "    str: 查询结果或错误信息\n",
    "    \"\"\"\n",
    "    # 使用数据库实例 db 来执行查询，run_no_throw 方法保证即使查询失败也不会抛出异常\n",
    "    result = db.run_no_throw(query)\n",
    "    \n",
    "    # 如果查询没有返回结果，提示错误信息\n",
    "    if not result:\n",
    "        return \"Error: Query failed. Please rewrite your query and try again.\"\n",
    "    \n",
    "    # 如果查询成功，返回结果\n",
    "    return result\n",
    "\n",
    "# 测试 SQL 查询工具\n",
    "print(db_query_tool.invoke(\"SELECT * FROM Artist LIMIT 10;\"))  # 执行查询并打印结果"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "#### 输出应如下所示：\n",
    "\n",
    "\n",
    "```sql\n",
    "[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\n",
    "\n",
    "```\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 定义查询检查工具 `query_check`\n",
    "\n",
    "下面构建一个带有 SQL 查询检查功能的智能体。它会首先检查 SQL 查询的正确性，并根据检查结果进行调整，确保查询语法和逻辑正确，然后再执行查询。\n",
    "\n",
    "这种模式非常适合在需要严格查询验证的应用场景中使用，例如复杂的数据库查询、自动化报表生成等。\n",
    "\n",
    "虽然这不是严格意义上的工具，但我们会用 LLM 来检查查询中的常见错误，并在工作流程中将其作为节点添加。\n",
    "\n",
    "### 代码说明：\n",
    "1. **`query_check_system` 提示模板**：\n",
    "   - 这是一个系统级的提示，模拟了一个具备 SQL 专业知识的角色。这个角色会检查用户提交的 SQL 查询，查找常见的错误（如 `NOT IN` 与 `NULL` 的组合、`BETWEEN` 用于排他范围、数据类型不匹配等）。\n",
    "   - 如果在查询中发现了错误，角色会重写查询。如果没有错误，则会直接返回原查询。\n",
    "\n",
    "2. **`ChatPromptTemplate` 定义**：\n",
    "   - `ChatPromptTemplate.from_messages` 通过系统消息和用户消息定义了一个对话模板。\n",
    "   - `system` 部分是系统角色，定义了如何检查 SQL 查询。\n",
    "   - `placeholder` 代表用户输入的占位符，最终将填充用户消息中的 SQL 查询。\n",
    "\n",
    "3. **工具绑定**：\n",
    "   - 使用 `query_llm.bind_tools([db_query_tool])` 绑定工具 `db_query_tool`。这样，提示模板可以在完成查询检查后，通过绑定的工具执行 SQL 查询。\n",
    "   - `tool_choice=\"required\"` 强制工具执行，这是确保 LLM 在每次检查后调用 `db_query_tool` 以执行查询的逻辑。\n",
    "\n",
    "4. **测试查询检查工具**：\n",
    "   - `query_check.invoke({\"messages\": [(\"user\", \"SELECT * FROM Artist LIMIT 10;\")]})` 是对查询检查工具的测试，用户输入查询 \"SELECT * FROM Artist LIMIT 10;\"，系统会先检查查询是否有问题，如果没有问题则调用 `db_query_tool` 执行查询。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Oge0lB8mAhQGg1mWr9dKFXOM', 'function': {'arguments': '{\"query\":\"SELECT * FROM Artist LIMIT 10;\"}', 'name': 'db_query_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 229, 'total_tokens': 249}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_25624ae3a5', 'finish_reason': 'stop', 'logprobs': None}, id='run-a3897a53-56a2-4966-9ae5-b927257033ee-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': 'SELECT * FROM Artist LIMIT 10;'}, 'id': 'call_Oge0lB8mAhQGg1mWr9dKFXOM', 'type': 'tool_call'}], usage_metadata={'input_tokens': 229, 'output_tokens': 20, 'total_tokens': 249})"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "\n",
    "# 定义用于检查 SQL 查询错误的系统提示模板\n",
    "query_check_system = \"\"\"You are a SQL expert with a strong attention to detail.\n",
    "Double check the SQLite query for common mistakes, including:\n",
    "- Using NOT IN with NULL values\n",
    "- Using UNION when UNION ALL should have been used\n",
    "- Using BETWEEN for exclusive ranges\n",
    "- Data type mismatch in predicates\n",
    "- Properly quoting identifiers\n",
    "- Using the correct number of arguments for functions\n",
    "- Casting to the correct data type\n",
    "- Using the proper columns for joins\n",
    "\n",
    "If there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n",
    "\n",
    "You will call the appropriate tool to execute the query after running this check.\"\"\"\n",
    "\n",
    "# 使用从系统消息和用户消息生成的模板来定义提示\n",
    "query_check_prompt = ChatPromptTemplate.from_messages(\n",
    "    [(\"system\", query_check_system),\n",
    "     (\"placeholder\", \"{messages}\")]\n",
    ")\n",
    "\n",
    "# 绑定工具 db_query_tool 到提示中，用于在检查查询后执行 SQL 查询\n",
    "query_check = query_check_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools([db_query_tool], tool_choice=\"required\")\n",
    "\n",
    "# 测试查询检查工具，提供用户查询并调用\n",
    "query_check.invoke({\"messages\": [(\"user\", \"SELECT * FROM Artist LIMIT 10;\")]})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 定义状态图（SQL Agent）\n",
    "\n",
    "下面使用 `LangChain` 和 `LangGraph` 来构建一个带有多步工作流的 SQL 查询智能体。从用户输入到查询生成、检查再到最终执行。每个步骤都是由工作流中的不同节点完成的，系统会根据当前状态自动决定下一个要执行的任务。\n",
    "\n",
    "通过这个工作流，智能体可以接收用户输入的问题，生成相应的 SQL 查询，检查查询的正确性，执行查询，并返回查询结果。\n",
    "\n",
    "这种设计非常适合用于自动化 SQL 查询生成与执行的场景，尤其是在查询语句复杂或容易出错的情况下。\n",
    "\n",
    "#### 代码说明：\n",
    "1. **定义 Agent 的状态**：\n",
    "   - 使用 `TypedDict` 定义了 `State`，表示智能体的状态，状态包含了一组消息列表，这些消息在整个工作流中传递和操作。\n",
    "\n",
    "2. **工作流程图的创建**：\n",
    "   - `workflow = StateGraph(State)` 用于创建工作流程图。整个工作流分为多个步骤，每个步骤都在一个节点中处理特定任务。\n",
    "\n",
    "3. **工具调用节点**：\n",
    "   - 第一个工具调用是 `sql_db_list_tables`，用于列出数据库中的表。通过 `first_tool_call` 函数构建了这个工具调用的节点，并且该节点通过消息的形式返回调用的结果。\n",
    "\n",
    "4. **SQL 查询的正确性检查**：\n",
    "   - `model_check_query` 函数用于检查生成的 SQL 查询是否正确，并根据检查结果决定是否继续生成新查询或者直接执行查询。\n",
    "\n",
    "5. **生成 SQL 查询**：\n",
    "   - `query_gen_node` 使用 GPT 模型生成 SQL 查询，调用 ChatPromptTemplate 提供的生成逻辑，将生成的 SQL 查询通过消息传递给工作流中的下一步。\n",
    "\n",
    "6. **条件判断**：\n",
    "   - `should_continue` 函数根据查询是否正确决定是否继续工作流程。如果查询中包含错误，流程将返回到重新生成查询的节点；如果没有错误，查询将被执行。\n",
    "\n",
    "7. **节点之间的连线**：\n",
    "   - `workflow.add_edge` 设置了每个节点的执行顺序。工作流从 `first_tool_call` 开始，经过多个节点进行表查询、架构获取、SQL 查询生成、查询检查和最终的查询执行，直到查询完成或出错为止。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated, Literal\n",
    "from langchain_core.messages import AIMessage\n",
    "from langchain_core.pydantic_v1 import BaseModel, Field\n",
    "from langchain_openai import ChatOpenAI\n",
    "from typing_extensions import TypedDict\n",
    "from langgraph.graph import END, StateGraph, START\n",
    "from langgraph.graph.message import AnyMessage, add_messages\n",
    "\n",
    "# 定义 Agent 的状态结构，存储消息信息\n",
    "class State(TypedDict):\n",
    "    messages: Annotated[list[AnyMessage], add_messages]\n",
    "\n",
    "# 定义一个新的工作流程图\n",
    "workflow = StateGraph(State)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 第一个工具调用的节点：获取数据库中的表信息\n",
    "def first_tool_call(state: State) -> dict[str, list[AIMessage]]:\n",
    "    return {\n",
    "        \"messages\": [\n",
    "            AIMessage(\n",
    "                content=\"\",  # 设置消息内容为空，工具调用会生成实际结果\n",
    "                tool_calls=[\n",
    "                    {\n",
    "                        \"name\": \"sql_db_list_tables\",  # 调用的工具名称，获取数据库表\n",
    "                        \"args\": {},  # 无参数调用\n",
    "                        \"id\": \"sql_list_tables\",  # 为工具调用生成唯一 ID\n",
    "                    }\n",
    "                ],\n",
    "            )\n",
    "        ]\n",
    "    }"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 检查 SQL 查询是否正确的节点\n",
    "def model_check_query(state: State) -> dict[str, list[AIMessage]]:\n",
    "    \"\"\"\n",
    "    使用该工具检查 SQL 查询的正确性。\n",
    "    \"\"\"\n",
    "    # 使用 query_check 来检查最后一个消息中的查询是否正确\n",
    "    return {\"messages\": [query_check.invoke({\"messages\": [state[\"messages\"][-1]]})]}\n",
    "\n",
    "# 将工具节点添加到工作流程\n",
    "# 第一个工具调用节点：列出表\n",
    "workflow.add_node(\"first_tool_call\", first_tool_call)  \n",
    "# 创建回退机制的工具节点：列出表\n",
    "workflow.add_node(\"list_tables_tool\", create_tool_node_with_fallback([list_tables_tool]))\n",
    "# 创建回退机制的工具节点：获取表架构\n",
    "workflow.add_node(\"get_schema_tool\", create_tool_node_with_fallback([get_schema_tool]))\n",
    "\n",
    "# 根据问题和数据库中的表选择相关表的节点，调用 LLM 来做出决定\n",
    "model_get_schema = ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools([get_schema_tool])\n",
    "\n",
    "# 通过 LLM 决定要使用的表\n",
    "workflow.add_node(\n",
    "    \"model_get_schema\",\n",
    "    lambda state: {\n",
    "        \"messages\": [model_get_schema.invoke(state[\"messages\"])],\n",
    "    },\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 定义最终提交答案的模型\n",
    "class SubmitFinalAnswer(BaseModel):\n",
    "    \"\"\"根据查询结果生成最终答案并提交给用户。\"\"\"\n",
    "    final_answer: str = Field(..., description=\"The final answer to the user\")\n",
    "\n",
    "# 用于生成 SQL 查询的提示模板\n",
    "query_gen_system = \"\"\"You are a SQL expert with a strong attention to detail.\n",
    "\n",
    "Given an input question, output a syntactically correct SQLite query to run, then look at the results of the query and return the answer.\n",
    "\n",
    "DO NOT call any tool besides SubmitFinalAnswer to submit the final answer.\n",
    "\n",
    "When generating the query:\n",
    "\n",
    "Output the SQL query that answers the input question without a tool call.\n",
    "\n",
    "Unless the user specifies a specific number of examples they wish to obtain, always limit your query to at most 5 results.\n",
    "You can order the results by a relevant column to return the most interesting examples in the database.\n",
    "Never query for all the columns from a specific table, only ask for the relevant columns given the question.\n",
    "\n",
    "If you get an error while executing a query, rewrite the query and try again.\n",
    "\n",
    "If you get an empty result set, you should try to rewrite the query to get a non-empty result set. \n",
    "NEVER make stuff up if you don't have enough information to answer the query... just say you don't have enough information.\n",
    "\n",
    "If you have enough information to answer the input question, simply invoke the appropriate tool to submit the final answer to the user.\n",
    "\n",
    "DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\"\"\"\n",
    "\n",
    "\n",
    "# 生成查询的提示模板，将生成的 SQL 查询提交到适当的工具\n",
    "query_gen_prompt = ChatPromptTemplate.from_messages(\n",
    "    [(\"system\", query_gen_system), (\"placeholder\", \"{messages}\")]\n",
    ")\n",
    "# 将生成的查询传递给最终答案的提交工具\n",
    "query_gen = query_gen_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n",
    "    [SubmitFinalAnswer, model_check_query]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 定义生成 SQL Query 节点\n",
    "\n",
    "`query_gen_node` 函数的设计目的是确保 LLM 在生成 SQL 查询时只调用正确的工具，即 `SubmitFinalAnswer`。如果 LLM 错误地调用了其他工具，系统会捕获该错误并提示用户进行修正。这种设计可以避免智能体因调用错误工具而出现不正确的行为，同时提高查询生成和工具调用的准确性。\n",
    "\n",
    "#### 代码解释：\n",
    "1. **`query_gen_node` 函数**：\n",
    "   - 该函数用于调用 `query_gen`（查询生成器）来生成 SQL 查询，并处理生成过程中可能出现的错误情况，特别是 LLM 生成的工具调用错误。\n",
    "\n",
    "2. **工具调用检查**：\n",
    "   - LLM 在生成 SQL 查询时，可能会调用一些工具，例如在生成查询后调用 `SubmitFinalAnswer` 工具来提交最终的查询结果。\n",
    "   - 由于 LLM 有时可能会产生 \"幻觉\"（即错误地调用了不相关的工具），该函数检查消息中的工具调用列表 `message.tool_calls`，并确保只有 `SubmitFinalAnswer` 被调用。\n",
    "\n",
    "3. **错误处理**：\n",
    "   - 如果 LLM 错误地调用了其他工具，函数会构建一个 `ToolMessage`，其中包含错误信息。消息内容将提示用户纠正错误，并且仅调用 `SubmitFinalAnswer` 来提交最终答案。\n",
    "\n",
    "4. **返回结果**：\n",
    "   - 函数返回一个包含生成的消息和可能的错误消息的字典。返回的格式是：`{\"messages\": [message] + tool_messages}`，其中 `message` 是生成的 SQL 查询结果，而 `tool_messages` 是处理可能出现的工具调用错误的消息列表。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 生成查询的节点，处理生成 SQL 查询的逻辑\n",
    "def query_gen_node(state: State):\n",
    "    # 调用 query_gen 来生成 SQL 查询\n",
    "    message = query_gen.invoke(state)\n",
    "\n",
    "    # 有时 LLM（大型语言模型）会产生幻觉，错误调用工具。我们需要捕获这些错误并返回相应的错误信息。\n",
    "    tool_messages = []\n",
    "    \n",
    "    # 检查生成的消息中是否有工具调用\n",
    "    if message.tool_calls:\n",
    "        # 遍历所有工具调用\n",
    "        for tc in message.tool_calls:\n",
    "            # 如果工具调用的名称不是 \"SubmitFinalAnswer\"，则这是一个错误\n",
    "            if tc[\"name\"] != \"SubmitFinalAnswer\":\n",
    "                # 构建一个错误消息，提示用户仅应调用 SubmitFinalAnswer\n",
    "                tool_messages.append(\n",
    "                    ToolMessage(\n",
    "                        content=f\"Error: The wrong tool was called: {tc['name']}. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.\",  # 错误内容\n",
    "                        tool_call_id=tc[\"id\"],  # 工具调用的唯一 ID\n",
    "                    )\n",
    "                )\n",
    "    else:\n",
    "        # 如果没有工具调用，工具消息列表为空\n",
    "        tool_messages = []\n",
    "\n",
    "    # 返回生成的消息和可能的错误消息，作为字典返回\n",
    "    return {\"messages\": [message] + tool_messages}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 将查询生成节点添加到工作流程\n",
    "workflow.add_node(\"query_gen\", query_gen_node)\n",
    "\n",
    "# 将查询检查和执行节点添加到工作流程\n",
    "workflow.add_node(\"correct_query\", model_check_query)  # 检查查询是否正确\n",
    "workflow.add_node(\"execute_query\", create_tool_node_with_fallback([db_query_tool]))  # 执行查询"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 定义条件判断，决定工作流程是否继续或结束\n",
    "def should_continue(state: State) -> Literal[END, \"correct_query\", \"query_gen\"]:\n",
    "    last_message = state[\"messages\"][-1]  # 获取最后一条消息\n",
    "    if getattr(last_message, \"tool_calls\", None):  # 如果有工具调用，工作流结束\n",
    "        return END\n",
    "    if last_message.content.startswith(\"Error:\"):  # 如果有错误，返回生成查询节点重新生成\n",
    "        return \"query_gen\"\n",
    "    else:\n",
    "        return \"correct_query\"  # 如果没有错误，则继续检查查询"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 设置节点之间的连线，定义工作流程的执行顺序\n",
    "workflow.add_edge(START, \"first_tool_call\")  # 开始节点连接到第一个工具调用节点\n",
    "workflow.add_edge(\"first_tool_call\", \"list_tables_tool\")  # 调用列出表的工具\n",
    "workflow.add_edge(\"list_tables_tool\", \"model_get_schema\")  # 根据问题决定使用哪个表\n",
    "workflow.add_edge(\"model_get_schema\", \"get_schema_tool\")  # 获取表的架构\n",
    "workflow.add_edge(\"get_schema_tool\", \"query_gen\")  # 根据表架构生成查询\n",
    "# 根据检查结果决定是否继续\n",
    "workflow.add_conditional_edges(\"query_gen\", should_continue)  \n",
    "workflow.add_edge(\"correct_query\", \"execute_query\")  # 检查查询后执行查询\n",
    "workflow.add_edge(\"execute_query\", \"query_gen\")  # 查询执行后，继续生成查询"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 将工作流程编译为可运行的应用\n",
    "app = workflow.compile()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 可视化流程图\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/jpeg": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "try:\n",
    "    display(Image(app.get_graph().draw_mermaid_png()))\n",
    "except Exception:\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 运行 Agent\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "Which sales agent made the most in sales in 2009?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_list_tables (sql_list_tables)\n",
      " Call ID: sql_list_tables\n",
      "  Args:\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_list_tables\n",
      "\n",
      "Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_schema (call_jqdtZvplscKL8WFOH4Qi20ge)\n",
      " Call ID: call_jqdtZvplscKL8WFOH4Qi20ge\n",
      "  Args:\n",
      "    table_names: Employee, Invoice\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_schema\n",
      "\n",
      "\n",
      "CREATE TABLE \"Employee\" (\n",
      "\t\"EmployeeId\" INTEGER NOT NULL, \n",
      "\t\"LastName\" NVARCHAR(20) NOT NULL, \n",
      "\t\"FirstName\" NVARCHAR(20) NOT NULL, \n",
      "\t\"Title\" NVARCHAR(30), \n",
      "\t\"ReportsTo\" INTEGER, \n",
      "\t\"BirthDate\" DATETIME, \n",
      "\t\"HireDate\" DATETIME, \n",
      "\t\"Address\" NVARCHAR(70), \n",
      "\t\"City\" NVARCHAR(40), \n",
      "\t\"State\" NVARCHAR(40), \n",
      "\t\"Country\" NVARCHAR(40), \n",
      "\t\"PostalCode\" NVARCHAR(10), \n",
      "\t\"Phone\" NVARCHAR(24), \n",
      "\t\"Fax\" NVARCHAR(24), \n",
      "\t\"Email\" NVARCHAR(60), \n",
      "\tPRIMARY KEY (\"EmployeeId\"), \n",
      "\tFOREIGN KEY(\"ReportsTo\") REFERENCES \"Employee\" (\"EmployeeId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Employee table:\n",
      "EmployeeId\tLastName\tFirstName\tTitle\tReportsTo\tBirthDate\tHireDate\tAddress\tCity\tState\tCountry\tPostalCode\tPhone\tFax\tEmail\n",
      "1\tAdams\tAndrew\tGeneral Manager\tNone\t1962-02-18 00:00:00\t2002-08-14 00:00:00\t11120 Jasper Ave NW\tEdmonton\tAB\tCanada\tT5K 2N1\t+1 (780) 428-9482\t+1 (780) 428-3457\tandrew@chinookcorp.com\n",
      "2\tEdwards\tNancy\tSales Manager\t1\t1958-12-08 00:00:00\t2002-05-01 00:00:00\t825 8 Ave SW\tCalgary\tAB\tCanada\tT2P 2T3\t+1 (403) 262-3443\t+1 (403) 262-3322\tnancy@chinookcorp.com\n",
      "3\tPeacock\tJane\tSales Support Agent\t2\t1973-08-29 00:00:00\t2002-04-01 00:00:00\t1111 6 Ave SW\tCalgary\tAB\tCanada\tT2P 5M5\t+1 (403) 262-3443\t+1 (403) 262-6712\tjane@chinookcorp.com\n",
      "*/\n",
      "\n",
      "\n",
      "CREATE TABLE \"Invoice\" (\n",
      "\t\"InvoiceId\" INTEGER NOT NULL, \n",
      "\t\"CustomerId\" INTEGER NOT NULL, \n",
      "\t\"InvoiceDate\" DATETIME NOT NULL, \n",
      "\t\"BillingAddress\" NVARCHAR(70), \n",
      "\t\"BillingCity\" NVARCHAR(40), \n",
      "\t\"BillingState\" NVARCHAR(40), \n",
      "\t\"BillingCountry\" NVARCHAR(40), \n",
      "\t\"BillingPostalCode\" NVARCHAR(10), \n",
      "\t\"Total\" NUMERIC(10, 2) NOT NULL, \n",
      "\tPRIMARY KEY (\"InvoiceId\"), \n",
      "\tFOREIGN KEY(\"CustomerId\") REFERENCES \"Customer\" (\"CustomerId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Invoice table:\n",
      "InvoiceId\tCustomerId\tInvoiceDate\tBillingAddress\tBillingCity\tBillingState\tBillingCountry\tBillingPostalCode\tTotal\n",
      "1\t2\t2009-01-01 00:00:00\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t1.98\n",
      "2\t4\t2009-01-02 00:00:00\tUllevålsveien 14\tOslo\tNone\tNorway\t0171\t3.96\n",
      "3\t8\t2009-01-03 00:00:00\tGrétrystraat 63\tBrussels\tNone\tBelgium\t1000\t5.94\n",
      "*/\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "\n",
      "Error: The wrong tool was called: model_check_query. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "```sql\n",
      "SELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\n",
      "FROM Employee e\n",
      "JOIN Customer c ON e.EmployeeId = c.SupportRepId\n",
      "JOIN Invoice i ON c.CustomerId = i.CustomerId\n",
      "WHERE strftime('%Y', i.InvoiceDate) = '2009'\n",
      "GROUP BY e.EmployeeId\n",
      "ORDER BY TotalSales DESC\n",
      "LIMIT 1;\n",
      "```\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  db_query_tool (call_wftQdpcuObQQrRpFIkmMwufG)\n",
      " Call ID: call_wftQdpcuObQQrRpFIkmMwufG\n",
      "  Args:\n",
      "    query: SELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\n",
      "FROM Employee e\n",
      "JOIN Customer c ON e.EmployeeId = c.SupportRepId\n",
      "JOIN Invoice i ON c.CustomerId = i.CustomerId\n",
      "WHERE strftime('%Y', i.InvoiceDate) = '2009'\n",
      "GROUP BY e.EmployeeId\n",
      "ORDER BY TotalSales DESC\n",
      "LIMIT 1;\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: db_query_tool\n",
      "\n",
      "[('Steve', 'Johnson', 164.33999999999997)]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  SubmitFinalAnswer (call_fS2L4JXlzY9yeMQmJhFSX3YN)\n",
      " Call ID: call_fS2L4JXlzY9yeMQmJhFSX3YN\n",
      "  Args:\n",
      "    final_answer: The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.\n"
     ]
    }
   ],
   "source": [
    "query = \"Which sales agent made the most in sales in 2009?\"\n",
    "\n",
    "events = app.stream(\n",
    "    {\"messages\": [(\"user\", query)]},\n",
    "    stream_mode=\"values\"  # stream_mode 设置为 \"values\"，表示返回流式数据的值\n",
    ")\n",
    "\n",
    "# 遍历每个事件，并打印最后一条消息的内容\n",
    "for event in events:\n",
    "    # 通过 pretty_print 打印最后一条消息的内容\n",
    "    event[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
